#include "filesystem.h"
#include "filesystem_exception.h"

#include <cstring>
#include <sys/stat.h>
#include <dirent.h>

namespace netaos {
namespace nosal {
namespace exec {
namespace internal {

const std::string PATH_SEPARATOR("/");
enum {
	DT_UNKNOWN = 0,
	DT_DIR = 4,
	DT_REG = 8
};

namespace
{

/// @brief Finds both files or subdirectories at the given path.
///
/// @tparam DT A type of the searching node. Only DT_DIR and DT_REG are valid values.
/// @param dirPath  A path to a directory where to search for both files or subdirectories.
/// @returns A vector of strings containing the file/sub-directory names.
/// @throws FilesystemException in case of error.
/// @note This call wrappes the opendir and readdir system calls.
template <unsigned char DT>
std::vector<std::string> Find(const std::string& dirPath);

/// @brief Checks if the passed dir entry has the specified type.
///
/// @tparam DT A type of the searching node. Only DT_DIR and DT_REG are valid values.
/// @param dirEntry A pointer to struct dirent of a node which should be checked for the specified type.
/// @param dirBase  A base path to the checking node.
/// @returns True is the passed dir entry has the specified type, false otherwise.
/// @throws FilesystemException in case of error.
template <unsigned char DT>
bool IsSameType(const dirent* dirEntry, const std::string& dirBase);

/// @brief Checks if current or parent directory.
///
/// @param nodeName A name of a node.
/// @returns True if any of those directory, false otherwise.
bool IsCurrentOrParentDir(const std::string& nodeName);

/// @brief Eliminate characters from the left and right end of a string.
///
/// Trims all characters occuring in charsToTrim from the left and right end of text. text is modified.
///
/// More formally: At either end of text the longest substring consisting only of characters listed in charsToTrim is
/// removed.
///
/// @param[inout] text The string to be trimmed
/// @param[in] charsToTrim the character to remove.
void trim(std::string& text, const std::string& charsToTrim);

}  // namespace

std::vector<std::string> Filesystem::GetSubdirectories(const std::string& path)
{
    return Find<DT_DIR>(path);
}

std::vector<std::string> Filesystem::GetFiles(const std::string& path)
{
    return Find<DT_REG>(path);
}

std::vector<std::string> Filesystem::GetFilesStartingWith(const std::string& path, const std::string& prefix)
{
    std::vector<std::string> filePaths = GetFiles(path);
    std::vector<std::string> filePathsFiltered;
    for (auto filePath : filePaths) {
        if (ExtractFileName(filePath).compare(0, prefix.size(), prefix) == 0) {
            filePathsFiltered.push_back(filePath);
        }
    }
    return filePathsFiltered;
}

std::string Filesystem::BuildPath(const std::string& base, const std::string& additional)
{
    std::string path(base);
    path += PATH_SEPARATOR;
    path += additional;
    return path;
}

std::string Filesystem::ExtractFileName(const std::string& path)
{
    std::string nodeName(path);
    trim(nodeName, PATH_SEPARATOR);
    size_t const position = nodeName.find_last_of(PATH_SEPARATOR);
    if (position != std::string::npos) {
        nodeName = nodeName.substr(position + 1, std::string::npos);
    }
    return nodeName;
}

namespace
{

template <unsigned char DT>
std::vector<std::string> Find(const std::string& dirPath)
{
    DIR* const dirStream = opendir(dirPath.c_str());
    if (dirStream == NULL) {
        throw FilesystemException(dirPath, errno);
    }

    std::vector<std::string> dirList;
    bool isSearching = true;
    while (isSearching == true) {
        const dirent* dirEntry = readdir(dirStream);
        if (dirEntry == NULL) {
            isSearching = false;
            continue;
        }
        if (IsCurrentOrParentDir(dirEntry->d_name)) {
            continue;
        }
        if (not IsSameType<DT>(dirEntry, dirPath)) {
            continue;
        }
        const std::string childNodePath = Filesystem::BuildPath(dirPath, dirEntry->d_name);
        dirList.push_back(childNodePath);
    }

    return dirList;
}

template <unsigned char DT>
bool IsSameType(const dirent* const dirEntry, const std::string& dirBase)
{
    static_assert(DT == DT_REG || DT == DT_DIR, "Only DT_REG and DT_DIR are supported.");
#if 0
    if (DT_UNKNOWN != dirEntry->d_type) {
        return DT == dirEntry->d_type;
    }
#endif
    const std::string nodePath = Filesystem::BuildPath(dirBase, dirEntry->d_name);
    struct stat nodeStat;
    std::memset(&nodeStat, 0, sizeof nodeStat);
    if (stat(nodePath.c_str(), &nodeStat) != 0) {
        throw FilesystemException(nodePath, errno);
    }

    return ((DT_DIR == DT && S_ISDIR(nodeStat.st_mode) != 0) || (DT_REG == DT && S_ISREG(nodeStat.st_mode != 0)));
}

bool IsCurrentOrParentDir(const std::string& nodeName)
{
    return (nodeName == ".") || (nodeName == "..");
}

void trim(std::string& text, const std::string& charsToTrim)
{
    std::size_t firstChar = text.find_first_not_of(charsToTrim);
    std::size_t lastChar = text.find_last_not_of(charsToTrim);
    if (firstChar == 0 && lastChar == text.length() - 1) {
        return;
    } else if ((firstChar == std::string::npos && lastChar == std::string::npos) || (lastChar < firstChar)) {
        // This string is either empty or contains nothing but trimmable characters
        text = std::string();
    } else {
        text = text.substr(firstChar, lastChar - firstChar + 1);
    }
}

}  //  namespace

}  //  namespace internal
}  //  namespace exec
}  //  namespace nosal
}  //  namespace netaos
